status: Add --json output
authorColin Walters <walters@verbum.org>
Thu, 26 Jun 2025 13:59:28 +0000 (09:59 -0400)
committerColin Walters <walters@verbum.org>
Thu, 26 Jun 2025 14:40:42 +0000 (10:40 -0400)
This is way long overdue, and will be useful for a variety of things
but is especially motivated by soft reboot testing.

Signed-off-by: Colin Walters <walters@verbum.org>
ci/gh-install.sh
ci/installdeps.sh
man/ostree-admin-status.xml
src/ostree/ot-admin-builtin-status.c
tests/admin-test.sh

index 2260883becd233d1edd503f39c01ce1b73549495..014799d13a01312c2707f290514256a8bdcf51c0 100755 (executable)
@@ -101,6 +101,7 @@ case "$ID" in
             libsystemd-dev
             libtool
             libcap2-bin
+            jq
             procps
             python3
             python3-yaml
index bb8a93b47985b7b8011b384bc51bcf22d727d32b..a370f9c9c85e0b2903a0323df186c1d824d5dde7 100755 (executable)
@@ -22,7 +22,7 @@ pkg_builddep ostree
 pkg_install composefs-devel
 pkg_install sudo which attr fuse strace \
     libubsan libasan libtsan redhat-rpm-config \
-    elfutils fsverity-utils
+    elfutils fsverity-utils jq
 if test -n "${CI_PKGS:-}"; then
     pkg_install ${CI_PKGS}
 fi
index aefa14db7a6bb88e20be86bfdf2815c87172e9ba..3a4877aff53b765c240b0c439acd722b7a335200 100644 (file)
@@ -81,6 +81,14 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
                 </para></listitem>
             </varlistentry>
 
+            <varlistentry>
+                <term><option>--json</option></term>
+
+                <listitem><para>
+                    Output in JSON format.
+                </para></listitem>
+            </varlistentry>
+
             <varlistentry>
                 <term><option>-S, --skip-signatures</option></term>
 
index d05d9928816ec76b1c15ae57d3353397bbed412e..63e0f3f4a7fe2ac8e62e2f0d697052b05a0b5ab9 100644 (file)
 #include "ostree.h"
 #include "ot-admin-builtins.h"
 #include "ot-admin-functions.h"
+#include "ul-jsonwrt.h"
 
 #include <glib/gi18n.h>
 
 static gboolean opt_verify;
 static gboolean opt_skip_signatures;
 static gboolean opt_is_default;
+static gboolean opt_json;
 
 static GOptionEntry options[]
     = { { "verify", 'V', 0, G_OPTION_ARG_NONE, &opt_verify, "Print the commit verification status",
           NULL },
+        { "json", 'J', 0, G_OPTION_ARG_NONE, &opt_json, "Emit JSON", NULL },
         { "skip-signatures", 'S', 0, G_OPTION_ARG_NONE, &opt_skip_signatures,
           "Skip signatures in output", NULL },
         { "is-default", 'D', 0, G_OPTION_ARG_NONE, &opt_is_default,
           "Output \"default\" if booted into the default deployment, otherwise \"not-default\"",
           NULL },
         { NULL } };
+
 static gboolean
 deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment,
                          gboolean is_booted, gboolean is_pending, gboolean is_rollback,
@@ -182,6 +186,45 @@ deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploym
   return TRUE;
 }
 
+static gboolean
+deployment_write_json (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment,
+                       gboolean is_booted, gboolean is_pending, gboolean is_rollback,
+                       struct ul_jsonwrt *jo, GCancellable *cancellable, GError **error)
+{
+  ul_jsonwrt_object_open (jo, NULL);
+
+  const char *ref = ostree_deployment_get_csum (deployment);
+  ul_jsonwrt_value_s (jo, "checksum", ref);
+  ul_jsonwrt_value_s (jo, "stateroot", ostree_deployment_get_osname (deployment));
+  ul_jsonwrt_value_u64 (jo, "serial", ostree_deployment_get_deployserial (deployment));
+  ul_jsonwrt_value_boolean (jo, "booted", is_booted);
+  ul_jsonwrt_value_boolean (jo, "pending", is_pending);
+  ul_jsonwrt_value_boolean (jo, "rollback", is_rollback);
+  ul_jsonwrt_value_boolean (jo, "finalization-locked",
+                            ostree_deployment_is_finalization_locked (deployment));
+  ul_jsonwrt_value_boolean (jo, "staged", ostree_deployment_is_staged (deployment));
+  ul_jsonwrt_value_boolean (jo, "pinned", ostree_deployment_is_pinned (deployment));
+  OstreeDeploymentUnlockedState unlocked = ostree_deployment_get_unlocked (deployment);
+  ul_jsonwrt_value_s (jo, "unlocked", ostree_deployment_unlocked_state_to_string (unlocked));
+
+  g_autoptr (GVariant) commit = NULL;
+  if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, ref, &commit, error))
+    return FALSE;
+  g_autoptr (GVariant) commit_metadata = g_variant_get_child_value (commit, 0);
+  const char *version = NULL;
+  const char *source_title = NULL;
+  (void)g_variant_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_VERSION, "&s", &version);
+  (void)g_variant_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_SOURCE_TITLE, "&s",
+                          &source_title);
+  if (version)
+    ul_jsonwrt_value_s (jo, "version", version);
+  if (source_title)
+    ul_jsonwrt_value_s (jo, "source-title", source_title);
+
+  ul_jsonwrt_object_close (jo);
+  return TRUE;
+}
+
 gboolean
 ot_admin_builtin_status (int argc, char **argv, OstreeCommandInvocation *invocation,
                          GCancellable *cancellable, GError **error)
@@ -206,6 +249,26 @@ ot_admin_builtin_status (int argc, char **argv, OstreeCommandInvocation *invocat
   if (booted_deployment)
     ostree_sysroot_query_deployments_for (sysroot, NULL, &pending_deployment, &rollback_deployment);
 
+  if (opt_json)
+    {
+      struct ul_jsonwrt jo_buf;
+      ul_jsonwrt_init (&jo_buf, stdout, 0);
+      struct ul_jsonwrt *jo = &jo_buf;
+      ul_jsonwrt_root_open (jo);
+      ul_jsonwrt_array_open (jo, "deployments");
+      for (guint i = 0; i < deployments->len; i++)
+        {
+          OstreeDeployment *deployment = deployments->pdata[i];
+          if (!deployment_write_json (sysroot, repo, deployment, deployment == booted_deployment,
+                                      deployment == pending_deployment,
+                                      deployment == rollback_deployment, jo, cancellable, error))
+            return FALSE;
+        }
+      ul_jsonwrt_array_close (jo);
+      ul_jsonwrt_root_close (jo);
+      return TRUE;
+    }
+
   if (opt_is_default)
     {
       if (deployments->len == 0)
index 0c442cfd8bd2c6acb9f18243939a0a838c4663ed..079bd6cbcc54f89304a4d06e5d76501bb25b68f6 100644 (file)
@@ -71,6 +71,12 @@ assert_not_file_has_content status.txt "pending"
 assert_not_file_has_content status.txt "rollback"
 validate_bootloader
 
+# And verify our JSON export
+${CMD_PREFIX} ostree admin status --json > status.json
+jq -e '.deployments[0].pending == false' status.json
+jq -e '.deployments[0].rollback == false' status.json
+assert_streq "$(jq -r '.deployments[0].checksum' status.json)" "${rev}"
+
 if has_ostree_feature composefs; then
     if ! test -f sysroot/ostree/deploy/testos/deploy/*.0/.ostree.cfs; then
         fatal "missing composefs"